#include <errno.h>
#include <arpa/inet.h>

#include "global.h"
#include "my_memory.h"
#include "net/socket.h"
#include "log.h"
#include "net/netglobal.h"

// 当收到包时，本函数会被调用
void CSocket::Read_request_handler(lpconnection_t pConn){
    bool isflood = false;// 是否是FLOOD攻击

    // 收包，注意我们用的第二个和第三个参数，我们用的始终是这两个参数
    // 必须保证 c->precvbuf指向正确的收包位置，c->irecvlen指向正确的收包宽度
    ssize_t reco = Recvproc(pConn, pConn->precvbuf, pConn->irecvlen);
    if(reco <= 0){
        return;// 说明出现错误，Recvproc函数中已经处理过了
    }

    // 走到这里说明成功收到了一些字节，开始处理
    // 是刚建立连接的状态,收包头
    if(pConn->curStat == _PKG_HD_INIT){
        if(reco == m_iLenPkgHeader){
            // 正好收到完整包头，进行拆解（状态在下列函数中改变）
            Wait_request_handler_proc_p1(pConn, isflood);
        }
        // 收到的包头不完整，需要继续接收
        else{
            pConn->curStat = _PKG_HD_RECVING;// 更新状态为正在接收包头
            pConn->precvbuf = pConn->precvbuf + reco;// 缓冲区指针后移
            pConn->irecvlen = pConn->irecvlen - reco;// 可接受长度减少
        }
    }
    // 如果已经是在继续接受包头状态
    else if(pConn->curStat == _PKG_HD_RECVING){
        // 如果要求收到的宽度和实际收到的宽度相等
        if(pConn->irecvlen == reco){
            // 正好收到完整包头，进行拆解（状态在下列函数中改变）
            Wait_request_handler_proc_p1(pConn, isflood);
        }
        // 收到的包头不完整，需要继续接收
        else{
            pConn->precvbuf = pConn->precvbuf + reco;// 缓冲区指针后移
            pConn->irecvlen = pConn->irecvlen - reco;// 可接受长度减少
        }
    }
    // 是刚准备收包体的状态
    else if(pConn->curStat == _PKG_BD_INIT){
        if(reco == pConn->irecvlen){
            // 正好包体也收完整了
            if(m_floodAkEnable == 1){
                //Flood攻击检测是否开启
                isflood = TestFlood(pConn);
            }
            // 调用处理包体的函数
            Wait_request_handler_proc_plast(pConn, isflood);
        }
        // 收到的包体不完整
        else{
            pConn->curStat = _PKG_BD_RECVING;
            pConn->precvbuf = pConn->precvbuf + reco;// 缓冲区指针后移
            pConn->irecvlen = pConn->irecvlen - reco;// 可接受长度减少
        }
    }
    // 如果是正在接收包体的状态
    else if(pConn->curStat == _PKG_BD_RECVING){
        // 如果正好收完整了
        if(pConn->irecvlen == reco){
            if(m_floodAkEnable == 1){
                //Flood攻击检测是否开启
                isflood = TestFlood(pConn);
            }
            // 调用处理包体的函数
            Wait_request_handler_proc_plast(pConn, isflood);
        }
        // 还是没收完整就继续收
        else{
            pConn->precvbuf = pConn->precvbuf + reco;// 缓冲区指针后移
            pConn->irecvlen = pConn->irecvlen - reco;// 可接受长度减少
        }
    }

    // 如果检查发现是泛洪攻击
    if(isflood == true){
        Log_Info(0, "SECURITY: 连接[%d] 被判定为Flood攻击，即将关闭", pConn->fd);
        // 直接把这个客户端踢掉
        ZdClosesocketProc(pConn);
    }
}

// 接收字节函数
ssize_t CSocket::Recvproc(lpconnection_t pConn, char *buff, ssize_t buflen){
    ssize_t n;// 保存接收字节数
    // 调用recv()系统函数真正接收字节， 最后一个参数flag，一般为0；
    n = recv(pConn->fd, buff, buflen, 0);

    // 返回值为0说明客户端主动断开了连接
    if(n == 0){
        ZdClosesocketProc(pConn);
        return -1;
    }
    // 返回值小于0说明有错误发生
    if(n < 0){
        // EAGAIN和EWOULDBLOCK都表示没收到数据
        // 在ET模式(边缘触发)下会出现这个错误，因为ET模式下是不停的recv肯定有一个时刻收到这个errno
        // 但在LT模式(水平触发)下一般是来事件才收，所以不该出现这个返回值
        if(errno == EAGAIN || errno == EWOULDBLOCK){
            Log_Error(errno,"CSocket::recvproc()中errno == EAGAIN || errno == EWOULDBLOCK成立，出乎我意料！");
            return -1; //不当做错误处理，只是简单返回
        }
        // 这个也不算错误,LT模式不该出现这个errno
        if(errno == EINTR){
            Log_Error(errno,"CSocket::recvproc()中errno == EINTR成立，出乎我意料！");
            return -1; //不当做错误处理，只是简单返回
        }

        // 其他错误都算作异常，关闭套接字
        ZdClosesocketProc(pConn);
        return -1;
    }

    // 说明收到了有效字节数
    return n;
}

// 包头收完整后的处理函数，成为包处理阶段1（p1）
// 注意参数isflood是引用
void CSocket::Wait_request_handler_proc_p1(lpconnection_t pConn,bool &isflood){
    CMemory *p_memory = CMemory::GetInstance();

    LPCOMM_PKG_HEADER pPkgHeader;// 包头结构体
    pPkgHeader = (LPCOMM_PKG_HEADER)pConn->dataHeadInfo;// precvbuf指针指向的就是这里

    unsigned short e_pkgLen;// 记录主机序的包总长度（包头+包体）
    e_pkgLen = ntohs(pPkgHeader->pkgLen);

    // 判断是否是恶意包或者错误包
    // 收到的报文总长度居然比包头小，那肯定是不对了，重置数据收下一个包
    if(e_pkgLen < m_iLenPkgHeader){
        pConn->curStat = _PKG_HD_INIT;
        pConn->precvbuf = pConn->dataHeadInfo;
        pConn->irecvlen = m_iLenPkgHeader;
    }
    // 报文总长度大于29000，肯定是恶意包（虽然上限是30000，但是29000已经够离谱了）
    else if(e_pkgLen > (_PKG_MAX_LENGTH - 1000)){
        pConn->curStat = _PKG_HD_INIT;
        pConn->precvbuf = pConn->dataHeadInfo;
        pConn->irecvlen = m_iLenPkgHeader;
    }
    // 否则就是合法的包头，继续进行下一步处理
    else{
        // 分配内存存包体，因为包体长度不定，长度是：消息头 + 包头 + 包体
        char *pTmpBuffer = (char *)p_memory->AllocMemory(m_iLenMsgHeader + e_pkgLen, false);
        pConn->precvMemPointer = pTmpBuffer;// 让内部指针指向

        // 1填写消息头内容（消息头是让自己看的）
        LPSTRUC_MSG_HEADER ptmpMsgHeader = (LPSTRUC_MSG_HEADER)pTmpBuffer;
        ptmpMsgHeader->pConn = pConn;
        ptmpMsgHeader->iCurrsequence = pConn->iCurrsequence;// 记录连接序号，以备后续比较

        // 2填写包头内容
        pTmpBuffer += m_iLenMsgHeader;// 移动指针
        memcpy(pTmpBuffer, pPkgHeader, m_iLenPkgHeader);// 拷贝包头内存到new的内存中
        // 如果报文总长度就等于包头长度，说明包体长度为0，认为这已经是一个完整的报文了
        if(e_pkgLen == m_iLenPkgHeader){
            if(m_floodAkEnable == 1) 
            {
                isflood = TestFlood(pConn);// 进行Flood攻击检测
            }
            // 进行下一步的处理
            Wait_request_handler_proc_plast(pConn, isflood);     
        }
        // 否则说明还没收到包体，准备开始接收包体
        else{
            pConn->curStat = _PKG_BD_INIT;// 改变状态
            pConn->precvbuf = pTmpBuffer + m_iLenPkgHeader;// 移动到包体开始位置（此时pTmpBuffer指向包头）
            pConn->irecvlen = e_pkgLen - m_iLenPkgHeader;// 得到包体的长度
        }
    }
}

// 处理完整的报文函数
// 注意参数isflood是引用
void CSocket::Wait_request_handler_proc_plast(lpconnection_t pConn,bool &isflood){
    // 判断是否是泛洪攻击
    if(isflood == false){
        // 通过全局线程池对象，将完整消息放入消息队列，并触发线程池中线程来处理该消息
        g_threadpool.InMsgRecvQueueAndSignal(pConn->precvMemPointer);
    }
    else{
        // 如果疑似是泛洪攻击，就直接丢掉，不放入消息队列
        CMemory *p_memory = CMemory::GetInstance();
        p_memory->FreeMemory(pConn->precvMemPointer);
    }

    // 处理完后重置数据
    pConn->precvMemPointer = NULL;
    pConn->curStat = _PKG_HD_INIT;
    pConn->precvbuf = pConn->dataHeadInfo;
    pConn->irecvlen = m_iLenPkgHeader;
}

// 发送数据的函数并返回发送的字节
// >0成功；=0对方断开
// -1：errno == EAGAIN，发送缓冲区满了
// -2: errno != EAGAIN != EWOULDBLOCK != EINTR,都当做是对端断开的错误
ssize_t CSocket::Sendproc(lpconnection_t c,char *buff,ssize_t size){
    ssize_t n;// 返回值

    // 类似while(true)，正常只循环一次
    for( ;; ){
        n = send(c->fd, buff, size, 0);// 系统调用发送，最后一个参数flag，一般为0；
        // 成功发送了一些数据
        if(n > 0){   
            return n; //返回本次发送的字节数
        }
        // 说明对方断开
        if(n == 0){

        }
        // 如果是发送缓冲区满
        if(errno == EAGAIN){
            return -1;
        }
        // 这个也不算错误，打印日志，然后等下一次循环发送
        if(errno == EINTR){ 
            Log_Error(errno,"CSocket::sendproc()中send()失败.");
        }
        else{
            // 走到这里是其他错误，但这里不主动断开socket，而是等待recv()来同一处理，避免线程竞争
            return -2;
        }
    }
}

// 数据发送时的写处理函数，当数据可写时由epoll通知并进行调用
// 当执行这个函数，说明是数据没办法发送完毕，需要用epoll驱动持续发送(理想情况是在发送线程中发送完的)
void CSocket::Write_request_handler(lpconnection_t pConn){
    CMemory *p_memory = CMemory::GetInstance();

    ssize_t sendsize = Sendproc(pConn,pConn->psendbuf,pConn->isendlen);

    // 没有全部发送完毕，数据只发出去了一部分
    if(sendsize > 0 && sendsize != pConn->isendlen){
        pConn->psendbuf = pConn->psendbuf + sendsize;// 更新发送位置
        pConn->isendlen = pConn->isendlen - sendsize;// 更新发送长度
        return;
    }
    else if(sendsize == -1){
        // 这个情况不太可能，因为你通知我发送，但是发送缓冲区是满的
        Log_Error(errno,"CSocket::Erite_request_handler()时if(sendsize == -1)成立，这很怪异。");
        return;
    }

    // 如果发送完毕，就移除掉写事件监听
    if(sendsize > 0 && sendsize == pConn->isendlen){
        if(Epoll_oper_event(pConn->fd, EPOLL_CTL_MOD, EPOLLOUT, 1, pConn) == -1){
            // 移除失败就打印日志,这个问题很麻烦，但是先不处理
            Log_Error(errno,"CSocket::Write_request_handler()中Epoll_oper_event()失败。");
        }
    }

    // 能执行到这里，说明要么数据发完了，要么对端断开了
    // 如果是数据发生完毕，说明发送缓冲区可能有地方了，唤醒线程干活并释放内存
    if(sem_post(&m_semEventSendQueue) == -1){
        Log_Error(0,"CSocket::Write_request_handler()中sem_post(&m_semEventSendQueue)失败.");
    }
    p_memory->FreeMemory(pConn->psendMemPointer);// 释放消息的内存
    pConn->psendMemPointer = NULL;
    --pConn->iThrowsendCount;   
}

// 虚函数给个默认实现，子类需要继承并重写
void CSocket::ThreadRecvProcFunc(char *pMsgBuf){
    return;
}




